---
category: Level 3 — Advanced
created: '2020-05-03'
keywords: 'custom scrollbar, drag to scroll, fake scrollbar, scrollbar-color, scrollbar-width, webkit-scrollbar, webkit-scrollbar-track, webkit-scrollbar-thumb'
title: Create a custom scrollbar
---

You can customize the look and feel of the browser's scrollbar by changing some CSS properties.
For example, we can use the `:-webkit-scrollbar` styles on the latest version of Chrome, Edge and Safari:

```css
body::-webkit-scrollbar {
    width: 0.75rem;
}
*::-webkit-scrollbar-track {
    background-color: #edf2f7;
}
*::-webkit-scrollbar-thumb {
    background-color: #718096;
    border-radius: 9999px;
}
```

On Firefox, we can use the new `scrollbar-width` and `scrollbar-color` styles:

```css
body {
    scrollbar-width: thin;
    /* The color of thumb and track areas */
    scrollbar-color: #718096 #edf2f7;
}
```

Unfortunately, the `-webkit-scrollbar` styles [aren't standard](https://developer.mozilla.org/en-US/docs/Web/CSS/::-webkit-scrollbar) and isn't recommended to use on production sites.

In this post, you'll see how to hide the default scrollbar and create a fake, customizable scrollbar. Assume that our target is a scrollable element whose `height` or `max-height` style is set:

```html
<div id="content" class="content" style="overflow: auto; max-height: ...;">...</div>
```

## Hide the default scrollbar

We wrap the content in a container which has the same `height` or `max-height` as the content. Instead of setting max height for the content, it'll take the full height.

```html
<div id="wrapper" class="wrapper">
    <div id="content" class="content">...</div>
</div>
```

We block the scrolling in the wrapper and still allow user to scroll in the content:

```css
.wrapper {
    max-height: 32rem;
    overflow: hidden;
}
.content {
    height: 100%;
    overflow: auto;
}
```

It's easy to hide the default scrollbar by using a negative margin:

```css
.content {
    margin-right: -1rem;
    padding-right: 1rem;
}
```

## Position the fake scrollbar

In this step, we'll create an element representing the fake scrollbar. It'll be positioned at the right side of the wrapper, and has the same height as wrapper.

To do so, we will use the third approach mentioned in the [Position an element absolutely to another element](https://phuoc.ng/collection/html-dom/position-an-element-absolutely-to-another-element/) post:

```html
<div id="wrapper">...</div>

<!-- Use an anchor -->
<div id="anchor" style="left: 0; position: absolute; top: 0"></div>

<!-- Fake scrollbar -->
<div id="scrollbar" style="position: absolute; width: .75rem;"></div>
```

The scrollbar is shown at the desired position by setting the `top` and `left` styles:

```js
// Query elements
const wrapper = document.getElementById('wrapper');
const content = document.getElementById('content');
const anchor = document.getElementById('anchor');
const scrollbar = document.getElementById('scrollbar');

// Get the bounding rectangles
const wrapperRect = wrapper.getBoundingClientRect();
const anchorRect = anchor.getBoundingClientRect();

// Set the scrollbar position
const top = wrapperRect.top - anchorRect.top;
const left = wrapperRect.width + wrapperRect.left - anchorRect.left;
scrollbar.style.top = `${top}px`;
scrollbar.style.left = `${left}px`;
```

The scrollbar has the same height as the wrapper:

```js
scrollbar.style.height = `${wrapperRect.height}px`;
```

## Organize the scrollbar

The scrollbar consists of two parts:

-   A track element that lets user know that there's a scrollbar. It takes the full size of scrollbar
-   A thumb element that user can click on and drag to scroll

```html
<div id="scrollbar">
    <div id="track" class="track"></div>
    <div id="thumb" class="thumb"></div>
</div>
```

These parts are positioned absolutely to the scrollbar, therefore they have the following styles:

```css
.track {
    left: 0;
    position: absolute;
    top: 0;

    /* Take full size */
    height: 100%;
    width: 100%;
}
.thumb {
    left: 0;
    position: absolute;

    /* Take full width */
    width: 100%;
}
```

Initially, the thumb's height is calculated based on the ratio between normal and scroll [heights](https://phuoc.ng/collection/html-dom/determine-the-height-and-width-of-an-element/) of the content element:

```js
// Query element
const track = document.getElementById('track');
const thumb = document.getElementById('thumb');

const scrollRatio = content.clientHeight / content.scrollHeight;
thumb.style.height = `${scrollRatio * 100}%`;
```

## Drag the thumb to scroll

Please visit the [Drag to scroll](https://phuoc.ng/collection/html-dom/drag-to-scroll/) post to see the details. Below is the implementation in our use case:

```js
let pos = { top: 0, y: 0 };

const mouseDownThumbHandler = function (e) {
    pos = {
        // The current scroll
        top: content.scrollTop,
        // Get the current mouse position
        y: e.clientY,
    };

    document.addEventListener('mousemove', mouseMoveHandler);
    document.addEventListener('mouseup', mouseUpHandler);
};

const mouseMoveHandler = function (e) {
    // How far the mouse has been moved
    const dy = e.clientY - pos.y;

    // Scroll the content
    content.scrollTop = pos.top + dy / scrollRatio;
};

// Attach the `mousedown` event handler
thumb.addEventListener('mousedown', mouseDownThumbHandler);
```

When user drags the thumb element as well as scroll the content element, we have to update the position of the thumb element.
Here is the `scroll` event handler of the content element:

```js
const scrollContentHandler = function () {
    window.requestAnimationFrame(function () {
        thumb.style.top = `${(content.scrollTop * 100) / content.scrollHeight}%`;
    });
};

content.addEventListener('scroll', scrollContentHandler);
```

## Jump when clicking the track

There is another way to scroll. User can jump in the content element by clicking a particular point in the track element.
Again, we have to calculate and update the `scrollTop` property for the content element:

```js
const trackClickHandler = function (e) {
    const bound = track.getBoundingClientRect();
    const percentage = (e.clientY - bound.top) / bound.height;
    content.scrollTop = percentage * (content.scrollHeight - content.clientHeight);
};

track.addEventListener('click', trackClickHandler);
```

I hope this post isn't too long and you can follow until here. Following is the final demo. Enjoy!

## Demo

<Playground>
```html
<div id="wrapper" style="border: 1px solid #cbd5e0; height: 32rem; margin: 2rem; overflow: hidden">
    <div
        id="content"
        style="height: 100%; margin-right: -1rem; overflow: auto; padding: 0.5rem; padding-right: 1rem"
    >
        <p style="margin-bottom: 1rem">
            Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean
            massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec
            quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim.
            Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut,
            imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium. Integer tincidunt.
            Cras dapibus. Vivamus elementum semper nisi. Aenean vulputate eleifend tellus. Aenean leo ligula,
            porttitor eu, consequat vitae, eleifend ac, enim. Aliquam lorem ante, dapibus in, viverra quis,
            feugiat a, tellus. Phasellus viverra nulla ut metus varius laoreet. Quisque rutrum. Aenean
            imperdiet. Etiam ultricies nisi vel augue. Curabitur ullamcorper ultricies nisi. Nam eget dui. Etiam
            rhoncus. Maecenas tempus, tellus eget condimentum rhoncus, sem quam semper libero, sit amet
            adipiscing sem neque sed ipsum. Nam quam nunc, blandit vel, luctus pulvinar, hendrerit id, lorem.
            Maecenas nec odio et ante tincidunt tempus. Donec vitae sapien ut libero venenatis faucibus. Nullam
            quis ante. Etiam sit amet orci eget eros faucibus tincidunt. Duis leo. Sed fringilla mauris sit amet
            nibh. Donec sodales sagittis magna.
        </p>
        <p style="margin-bottom: 1rem">
            Sed consequat, leo eget bibendum sodales, augue velit cursus nunc, quis gravida magna mi a libero.
            Fusce vulputate eleifend sapien. Vestibulum purus quam, scelerisque ut, mollis sed, nonummy id,
            metus. Nullam accumsan lorem in dui. Cras ultricies mi eu turpis hendrerit fringilla. Vestibulum
            ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; In ac dui quis mi
            consectetuer lacinia. Nam pretium turpis et arcu. Duis arcu tortor, suscipit eget, imperdiet nec,
            imperdiet iaculis, ipsum. Sed aliquam ultrices mauris. Integer ante arcu, accumsan a, consectetuer
            eget, posuere ut, mauris. Praesent adipiscing. Phasellus ullamcorper ipsum rutrum nunc. Nunc nonummy
            metus. Vestibulum volutpat pretium libero. Cras id dui. Aenean ut Lorem ipsum dolor sit amet,
            consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque
            penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec,
            pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. Donec pede justo, fringilla
            vel, aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, imperdiet a, venenatis vitae,
            justo. Nullam dictum felis eu pede mollis pretium. Integer tincidunt. Cras dapibus. Vivamus
            elementum semper nisi. Aenean vulputate eleifend tellus. Aenean leo ligula, porttitor eu, consequat
            vitae, eleifend ac, enim.
        </p>
        <p>
            Aliquam lorem ante, dapibus in, viverra quis, feugiat a, tellus. Phasellus viverra nulla ut metus
            varius laoreet. Quisque rutrum. Aenean imperdiet. Etiam ultricies nisi vel augue. Curabitur
            ullamcorper ultricies nisi. Nam eget dui. Etiam rhoncus. Maecenas tempus, tellus eget condimentum
            rhoncus, sem quam semper libero, sit amet adipiscing sem neque sed ipsum. Nam quam nunc, blandit
            vel, luctus pulvinar, hendrerit id, lorem. Maecenas nec odio et ante tincidunt tempus. Donec vitae
            sapien ut libero venenatis faucibus. Nullam quis ante Etiam sit amet orci eget eros faucibus
            tincidunt. Duis leo. Sed fringilla mauris sit amet nibh. Donec sodales sagittis magna. Sed
            consequat, leo eget bibendum sodales, augue velit cursus nunc, quis gravida magna mi a libero. Fusce
            vulputate eleifend sapien. Vestibulum purus quam, scelerisque ut, mollis sed, nonummy id, metus.
            Nullam accumsan lorem in dui. Cras ultricies mi eu turpis hendrerit fringilla. Vestibulum ante ipsum
            primis in faucibus orci luctus et ultrices posuere cubilia Curae; In ac dui quis mi consectetuer
            lacinia. Nam pretium turpis et arcu. Duis arcu tortor, suscipit eget, imperdiet nec, imperdiet
            iaculis, ipsum. Sed aliquam ultrices mauris. Integer ante arcu, accumsan a, consectetuer eget,
            posuere ut, mauris. Praesent adipiscing. Phasellus ullamcorper ipsum rutrum nunc. Nunc nonummy
            metus. Vestibulum volutpat pretium libero. Cras id dui. Aenean ut
        </p>
    </div>
</div>
<div id="anchor" class="anchor"></div>
<div id="scrollbar" class="scrollbar">
    <div id="track" class="track"></div>
    <div id="thumb" class="thumb"></div>
</div>
```

```css
.anchor {
    left: 0;
    position: absolute;
    top: 0;
}
.grabbing {
    cursor: grabbing;
    user-select: none;
}
.scrollbar {
    background-color: #edf2f7;
    border: 1px solid #cbd5e0;
    border-left-width: 0;
    position: absolute;
    width: 0.75rem;
}
.thumb {
    background-color: #718096;
    border-radius: 9999px;
    cursor: grab;
    left: 0;
    position: absolute;
    width: 100%;
}
.track {
    height: 100%;
    left: 0;
    position: absolute;
    top: 0;
    width: 100%;
}   
```

```js
document.addEventListener('DOMContentLoaded', function () {
    // Query elements
    const wrapper = document.getElementById('wrapper');
    const content = document.getElementById('content');
    const anchor = document.getElementById('anchor');
    const scrollbar = document.getElementById('scrollbar');
    const track = document.getElementById('track');
    const thumb = document.getElementById('thumb');

    // Get the bounding rectangles
    const wrapperRect = wrapper.getBoundingClientRect();
    const anchorRect = anchor.getBoundingClientRect();

    // Set the scrollbar position
    const top = wrapperRect.top - anchorRect.top;
    const left = wrapperRect.width + wrapperRect.left - anchorRect.left;
    scrollbar.style.top = top + 'px';
    scrollbar.style.left = left + 'px';

    // The scrollbar has the same height as the wrapper
    scrollbar.style.height = wrapperRect.height + 'px';

    // Set the initial height for thumb
    const scrollRatio = content.clientHeight / content.scrollHeight;
    thumb.style.height = scrollRatio * 100 + '%';

    let pos = { top: 0, y: 0 };

    const mouseDownThumbHandler = function (e) {
        pos = {
            // The current scroll
            top: content.scrollTop,
            // Get the current mouse position
            y: e.clientY,
        };

        thumb.classList.add('grabbing');
        document.body.classList.add('grabbing');

        document.addEventListener('mousemove', mouseMoveHandler);
        document.addEventListener('mouseup', mouseUpHandler);
    };

    const mouseMoveHandler = function (e) {
        // How far the mouse has been moved
        const dy = e.clientY - pos.y;

        // Scroll the content
        content.scrollTop = pos.top + dy / scrollRatio;
    };

    const mouseUpHandler = function (e) {
        thumb.classList.remove('grabbing');
        document.body.classList.remove('grabbing');

        document.removeEventListener('mousemove', mouseMoveHandler);
        document.removeEventListener('mouseup', mouseUpHandler);
    };

    const scrollContentHandler = function () {
        window.requestAnimationFrame(function () {
            thumb.style.top = (content.scrollTop * 100) / content.scrollHeight + '%';
        });
    };

    const trackClickHandler = function (e) {
        const bound = track.getBoundingClientRect();
        const percentage = (e.clientY - bound.top) / bound.height;
        content.scrollTop = percentage * (content.scrollHeight - content.clientHeight);
    };

    content.addEventListener('scroll', scrollContentHandler);
    thumb.addEventListener('mousedown', mouseDownThumbHandler);
    track.addEventListener('click', trackClickHandler);
});
```
</Playground>

## See also

-   [Attach or detach an event handler](https://phuoc.ng/collection/html-dom/attach-or-detach-an-event-handler/)
-   [Determine the height and width of an element](https://phuoc.ng/collection/html-dom/determine-the-height-and-width-of-an-element/)
-   [Drag to scroll](https://phuoc.ng/collection/html-dom/drag-to-scroll/)
-   [Position an element absolutely to another element](https://phuoc.ng/collection/html-dom/position-an-element-absolutely-to-another-element/)
